package dao

import (
	"context"
	"errors"
	"fmt"
	"github.com/redis/go-redis/v9"
	"gorm.io/gorm"
	"log"
	"time"
	"user-srv/basic/inits"
	__ "user-srv/basic/proto"
	"user-srv/handler/models"
)

// CheckInRule 签到积分规则
type CheckInRule struct {
	ContinuousDays int // 连续签到天数
	Points         int // 对应的积分
}

// GetRedisKey 获取Redis Bitmap的key
func GetRedisKey(userID int64, year int) string {
	return fmt.Sprintf("sign:user:%d:%d", userID, year)
}

// GetCheckInTimeKey 获取签到时间Redis键名
func GetCheckInTimeKey(userID int64) string {
	return fmt.Sprintf("sign:time:%d", userID)
}

// GetMakeupLimitKey 获取补签次数Redis键名
func GetMakeupLimitKey(userID int64) string {
	return fmt.Sprintf("sign:makeup_limit:%d", userID)
}

// GetStreakResetKey 获取积分重置Redis键名
func GetStreakResetKey(userID int64) string {
	return fmt.Sprintf("sign:streak_reset:%d", userID)
}

/*
参数:
  - in: 签到请求参数，包含用户ID等信息

返回值:
  - continuousDays: 当前连续签到天数
  - points: 本次签到获得的积分
  - err: 如果在签到过程中出现错误，将返回对应的错误信息

主要流程:
 1. 验证用户是否存在
 2. 检查今天是否已签到
 3. 执行签到操作（在Redis中设置对应位图）
 4. 设置签到记录过期时间
 5. 计算连续签到天数
 6. 检查是否有补签重置标记
 7. 根据连续签到天数计算应得积分
 8. 发放积分并记录日志
 9. 将积分信息存储到用户积分表和积分记录表
 10. 出错时进行回滚操作
*/
func CheckIn(in *__.CheckInReq) (continuousDays int, points int, err error) {
	var user models.User
	user.Id = uint32(in.UserId)
	if err := user.GetUserById(); err != nil {
		return 0, 0, errors.New("用户不存在")
	}
	now := time.Now()
	year := now.Year()
	dayOfYear := now.YearDay()

	// 获取Redis key
	key := GetRedisKey(in.UserId, year)

	// 检查今天是否已签到
	isChecked, err := inits.Rdb.GetBit(inits.Ctx, key, int64(dayOfYear-1)).Result()
	if err != nil {
		return 0, 0, fmt.Errorf("检查签到状态失败: %v", err)
	}

	if isChecked == 1 {
		return 0, 0, errors.New("今天已经签过到了")
	}

	// 执行签到 (设置对应bit为1)
	err = inits.Rdb.SetBit(inits.Ctx, key, int64(dayOfYear-1), 1).Err()
	if err != nil {
		return 0, 0, fmt.Errorf("签到失败: %v", err)
	}

	// 设置过期时间到年底
	endOfYear := time.Date(year, 12, 31, 23, 59, 59, 0, time.Local)
	expireDuration := endOfYear.Sub(now) + time.Hour*24
	inits.Rdb.Expire(context.Background(), key, expireDuration)

	// 计算连续签到天数
	continuousDays, err = CalculateContinuousDays(in.UserId, year, dayOfYear)
	if err != nil {
		// 回滚签到状态
		inits.Rdb.SetBit(context.Background(), key, int64(dayOfYear-1), 0)
		return 0, 0, fmt.Errorf("计算连续签到天数失败: %v", err)
	}
	// 检查是否断签
	isBroken := IsCheckInBroken(in.UserId, year, dayOfYear)
	// 检查积分是否因补签而重置
	resetKey := GetStreakResetKey(in.UserId)
	val, err := inits.Rdb.Get(inits.Ctx, resetKey).Result()

	effectiveContinuousDays := continuousDays
	if err == nil && val == "1" { // 如果存在重置标记
		effectiveContinuousDays = 1
		inits.Rdb.Del(inits.Ctx, resetKey) // 消耗标记
	}

	// 计算应得积分
	points = CalculatePoints(effectiveContinuousDays)
	// 如果断签，积分减半（根据业务规则）
	if isBroken {
		points = points / 2
	}
	// 发放积分并记录日志
	remark := fmt.Sprintf("连续签到%d天获得积分", continuousDays)

	err = AddPoints(in.UserId, points, remark)
	if err != nil {
		// 积分发放失败，回滚签到状态
		inits.Rdb.SetBit(inits.Ctx, key, int64(dayOfYear-1), 0)
		return 0, 0, fmt.Errorf("发放签到积分失败: %v", err)
	}

	// 更新用户积分表中的总签到天数
	var userPoints models.UserPoints
	userPoints.UserId = uint32(in.UserId)
	err = userPoints.GetUserPointsById()
	// 用户积分记录不存在
	if err != nil && err != gorm.ErrRecordNotFound {
		// 查询出错，记录错误但不影响签到流程
		fmt.Printf("查询用户积分记录失败: %v\n", err)
	} else if err != nil && err == gorm.ErrRecordNotFound {
		// 用户积分记录不存在，创建新记录
		userPoints = models.UserPoints{
			UserId:      uint32(in.UserId),
			TotalPoints: uint32(points),
			PointsDay:   1, // 首次签到，签到天数为1
			CreatedAt:   time.Now(),
		}
		if err := userPoints.AddUserPoints(); err != nil {
			// 记录错误但不影响签到流程
			fmt.Printf("创建用户积分记录失败: %v\n", err)
		}
	} else {
		// 用户积分记录存在，更新积分和签到天数
		userPoints.TotalPoints += uint32(points)
		userPoints.PointsDay += 1 // 每签到一天自动增加
		userPoints.UpdatedAt = time.Now()
		if err := userPoints.UpdateUserPoints(); err != nil {
			// 记录错误但不影响签到流程
			fmt.Printf("更新用户积分记录失败: %v\n", err)
		}
	}

	// 添加用户积分记录表
	userPointsLog := models.UserPointsLog{
		UserId: uint32(in.UserId),
		Points: uint32(points),
		Type:   "income",
		Reason: remark,
	}
	if err := userPointsLog.AddUserPointsLog(); err != nil {
		return 0, 0, fmt.Errorf("添加用户积分记录表失败: %v", err)
	}

	return continuousDays, points, nil
}

/*
返回值:
  - continuousDays: 当前连续签到天数
  - points: 本次签到获得的积分
  - err: 如果在签到过程中出现错误，将返回对应的错误信息

主要流程:
 1. 验证用户是否存在
 2. 检查今天是否已签到
 3. 执行签到操作（在Redis中设置对应位图）
 4. 设置签到记录过期时间
 5. 计算连续签到天数
 6. 检查是否有补签重置标记
 7. 根据连续签到天数计算应得积分
 8. 发放积分并记录日志
 9. 将积分信息存储到用户积分表和积分记录表
 10. 出错时进行回滚操作
*/
func GetCheckInList(in *__.GetCheckInListReq) ([]CheckInRecord, error) {
	// 验证年份和月份
	if in.Year <= 0 {
		return nil, errors.New("无效的年份")
	}
	if in.Month < 1 || in.Month > 12 {
		return nil, errors.New("无效的月份")
	}

	year := int(in.Year)
	month := int(in.Month)

	// 计算月份天数
	daysInMonth := time.Date(year, time.Month(month)+1, 0, 0, 0, 0, 0, time.Local).Day()

	// 获取Redis key
	key := GetRedisKey(in.UserId, year)

	var records []CheckInRecord
	// 使用Redis Pipeline来批量执行GETBIT命令，提高性能
	pipe := inits.Rdb.Pipeline()
	cmds := make([]*redis.IntCmd, daysInMonth)
	for day := 1; day <= daysInMonth; day++ {
		currentDate := time.Date(year, time.Month(month), day, 0, 0, 0, 0, time.Local)
		dayOfYear := currentDate.YearDay()
		cmds[day-1] = pipe.GetBit(inits.Ctx, key, int64(dayOfYear-1))
	}

	_, err := pipe.Exec(inits.Ctx)
	if err != nil {
		return nil, fmt.Errorf("获取签到数据失败: %v", err)
	}

	// 准备批量获取签到时间的pipeline
	timePipe := inits.Rdb.Pipeline()
	timeKey := GetCheckInTimeKey(in.UserId)
	timeCmds := make([]*redis.StringCmd, daysInMonth)

	for day := 1; day <= daysInMonth; day++ {
		currentDate := time.Date(year, time.Month(month), day, 0, 0, 0, 0, time.Local)
		dayOfYear := currentDate.YearDay()
		field := fmt.Sprintf("%d-%03d", year, dayOfYear)
		timeCmds[day-1] = timePipe.HGet(inits.Ctx, timeKey, field)
	}
	_, _ = timePipe.Exec(inits.Ctx) // Exec for time keys

	for day := 1; day <= daysInMonth; day++ {
		isChecked, err := cmds[day-1].Result()
		if err != nil {
			log.Printf("获取用户 %d 在 %d-%d-%d 的签到状态失败: %v", in.UserId, year, month, day, err)
			isChecked = 0
		}

		checkInTime, err := timeCmds[day-1].Result()
		if err == redis.Nil {
			checkInTime = ""
		} else if err != nil {
			log.Printf("获取用户 %d 在 %d-%d-%d 的签到时间失败: %v", in.UserId, year, month, day, err)
			checkInTime = ""
		}

		records = append(records, CheckInRecord{
			Day:         day,
			CheckedIn:   isChecked == 1,
			IsMakeup:    false, // 补签逻辑可单独实现
			CheckInTime: checkInTime,
		})
	}

	return records, nil
}

/*
签到积分规则：
- 连续签到1-2天：获得5积分
- 连续签到3-6天：获得10积分
- 连续签到7-14天：获得15积分
- 连续签到15天及以上：固定获得30积分

断签规则：
- 如果发生断签，当天签到获得的积分会减半（向下取整）。

补签规则：
- 只能补签过去7天内未签到的日期。
- 每次补签需要消耗3积分。
- 一个星期内只能补签一次。
- 补签后，连续签到天数会恢复，但下一次签到获得的积分将从第1天开始重新计算。
*/
func MakeUpCheckIn(userID int64, date time.Time) (bool, int, error) {
	// 1. 检查补签频率 (一星期只能补签一次)
	limitKey := GetMakeupLimitKey(userID)
	if exists, _ := inits.Rdb.Exists(inits.Ctx, limitKey).Result(); exists > 0 {
		return false, 0, errors.New("补签操作太频繁，请于7天后再试")
	}

	// 2. 检查补签有效期 (只能补签7天内的记录)
	now := time.Now()
	if now.Sub(date) > 7*24*time.Hour || date.After(now) {
		return false, 0, errors.New("只能补签7天内的记录")
	}

	year := date.Year()
	dayOfYear := date.YearDay()
	key := GetRedisKey(userID, year)

	// 3. 检查该日期是否已签到
	if isChecked, err := inits.Rdb.GetBit(inits.Ctx, key, int64(dayOfYear-1)).Result(); err != nil {
		return false, 0, fmt.Errorf("检查签到状态失败: %v", err)
	} else if isChecked == 1 {
		return false, 0, errors.New("该日期已经签过到了")
	}
	// 检查是否断签（用于记录日志）
	isBroken := IsCheckInBroken(userID, year, dayOfYear)
	// 4. 检查用户积分是否足够 (需要3积分)
	var userPoints models.UserPoints
	userPoints.UserId = uint32(userID)
	if err := userPoints.GetUserPointsById(); err != nil {
		if err == gorm.ErrRecordNotFound {
			return false, 0, errors.New("积分不足，无法补签（需要3积分）")
		}
		return false, 0, fmt.Errorf("查询用户积分失败: %v", err)
	}
	if userPoints.TotalPoints < 3 {
		return false, 0, errors.New("积分不足，无法补签（需要3积分）")
	}
	// a. 执行补签
	if err := inits.Rdb.SetBit(inits.Ctx, key, int64(dayOfYear-1), 1).Err(); err != nil {
		return false, 0, fmt.Errorf("执行补签失败: %v", err)
	}
	// b. 设置补签频率限制（7天）
	inits.Rdb.Set(inits.Ctx, limitKey, 1, 7*24*time.Hour)
	// c. 设置积分重置标记（24小时），这会影响下一次签到
	resetKey := GetStreakResetKey(userID)
	inits.Rdb.Set(inits.Ctx, resetKey, 1, 24*time.Hour)

	// 6. 更新数据库：扣除积分、增加总签到天数、记录日志
	deductPoints := 3
	reason := fmt.Sprintf("补签 %s 扣除积分", date.Format("2006-01-02"))

	userPoints.TotalPoints -= uint32(deductPoints)
	userPoints.PointsDay += 1 // 总签到天数增加
	userPoints.UpdatedAt = time.Now()

	if err := userPoints.UpdateUserPoints(); err != nil {
		// 数据库更新失败，回滚所有操作
		inits.Rdb.SetBit(inits.Ctx, key, int64(dayOfYear-1), 0) // 回滚签到
		inits.Rdb.Del(inits.Ctx, limitKey)                      // 移除频率限制
		inits.Rdb.Del(inits.Ctx, resetKey)                      // 移除重置标记
		return false, 0, fmt.Errorf("更新用户积分失败: %v", err)
	}

	// 添加积分消费日志
	reason = fmt.Sprintf("补签 %s 扣除积分", date.Format("2006-01-02"))
	if isBroken {
		reason += "（断签后补签）"
	}

	userPointsLog := models.UserPointsLog{
		UserId:    uint32(userID),
		Points:    uint32(deductPoints),
		Type:      "expense",
		Reason:    reason,
		CreatedAt: time.Now(),
	}
	if err := userPointsLog.AddUserPointsLog(); err != nil {
		fmt.Printf("添加积分消费日志失败: %v\n", err) // 记录错误，但不影响补签结果
	}

	// 7. 重新计算并返回最新的连续签到天数
	nowForCalc := time.Now()
	continuousDays, err := CalculateContinuousDays(userID, nowForCalc.Year(), nowForCalc.YearDay())
	if err != nil {
		fmt.Printf("补签后计算连续签到天数失败: %v\n", err)
		return true, 0, nil // 补签本身成功，但天数计算失败
	}

	return true, continuousDays, nil
}

func CalculatePoints(continuousDays int) int {
	switch {
	case continuousDays >= 15:
		return 30
	case continuousDays >= 7:
		return 15
	case continuousDays >= 3:
		return 10
	default: // 1-2天
		return 5
	}
}

// 计算连续签到天数
func CalculateContinuousDays(userID int64, year int, dayOfYear int) (int, error) {
	key := GetRedisKey(userID, year)
	count := 0

	// 从今天开始往前检查连续签到天数
	for i := 0; i < dayOfYear; i++ {
		bitPos := int64(dayOfYear - 1 - i)
		isChecked, err := inits.Rdb.GetBit(inits.Ctx, key, bitPos).Result()
		if err != nil {
			return 0, err
		}

		if isChecked == 1 {
			count++
		} else {
			break // 遇到未签到的日子，停止计数
		}
	}

	return count, nil
}

type CheckInRecord struct {
	Day         int  // 1-based
	CheckedIn   bool // 是否已签到
	IsMakeup    bool // 是否是补签
	CheckInTime string
}

// AddPoints 增加用户积分并记录日志
func AddPoints(userID int64, points int, reason string) error {
	// 使用事务确保积分添加和日志记录的原子性
	return inits.DB.Transaction(func(tx *gorm.DB) error {
		// 1. 更新用户总积分
		var userPoints models.UserPoints
		result := userPoints.GetUserPointsById()

		if result == gorm.ErrRecordNotFound {
			// 如果用户积分记录不存在，创建新记录
			userPoints = models.UserPoints{
				UserId:      userPoints.UserId,
				TotalPoints: userPoints.TotalPoints,
				CreatedAt:   time.Now(),
			}
			if err := tx.Create(&userPoints).Error; err != nil {
				return err
			}
		} else if result.Error != nil {
			// 其他查询错误
			return result
		} else {
			// 更新现有用户的积分
			userPoints.TotalPoints += uint32(points)
			userPoints.UpdatedAt = time.Now()
			if err := tx.Save(&userPoints).Error; err != nil {
				return err
			}
		}

		// 2. 记录积分日志
		pointsLog := models.UserPointsLog{
			UserId:    uint32(userID),
			Points:    uint32(points),
			Type:      "income", // 签到积分是收入
			Reason:    reason,
			CreatedAt: time.Now(),
		}
		if err := tx.Create(&pointsLog).Error; err != nil {
			return err
		}

		return nil
	})
}

// isCheckInBroken 检查用户是否断签
func IsCheckInBroken(userID int64, year int, dayOfYear int) bool {
	if dayOfYear > 1 {
		// 检查昨天是否签到
		key := GetRedisKey(userID, year)
		yesterdayChecked, err := inits.Rdb.GetBit(inits.Ctx, key, int64(dayOfYear-2)).Result()
		if err == nil && yesterdayChecked == 0 {
			// 昨天未签到，视为断签
			return true
		}
	} else if dayOfYear == 1 {
		// 今天是新年第一天，检查去年最后一天是否签到
		lastYear := year - 1
		lastYearKey := GetRedisKey(userID, lastYear)
		// 去年最后一天是第365天（或闰年的366天）
		lastDayOfYear := 365
		if time.Date(lastYear, 12, 31, 0, 0, 0, 0, time.Local).YearDay() == 366 {
			lastDayOfYear = 366
		}

		lastDayChecked, err := inits.Rdb.GetBit(inits.Ctx, lastYearKey, int64(lastDayOfYear-1)).Result()
		if err == nil && lastDayChecked == 0 {
			// 去年最后一天未签到，视为断签
			return true
		}
	}
	return false
}

/*
GetCheckInStatus 获取用户签到状态信息
返回用户的签到状态，包括是否已签到、是否断签等信息

参数:
  - userID: 用户ID
  - date: 要检查的日期，如果为零值则检查今天

返回值:
  - isChecked: 是否已签到
  - isBroken: 是否断签
  - continuousDays: 当前连续签到天数
  - err: 错误信息
*/
func GetCheckInStatus(userID int64, date time.Time) (isChecked bool, isBroken bool, continuousDays int, err error) {
	var checkDate time.Time
	if date.IsZero() {
		checkDate = time.Now()
	} else {
		checkDate = date
	}

	year := checkDate.Year()
	dayOfYear := checkDate.YearDay()
	key := GetRedisKey(userID, year)

	// 检查是否已签到
	bit, err := inits.Rdb.GetBit(inits.Ctx, key, int64(dayOfYear-1)).Result()
	if err != nil {
		return false, false, 0, fmt.Errorf("检查签到状态失败: %v", err)
	}
	isChecked = bit == 1

	// 检查是否断签
	isBroken = IsCheckInBroken(userID, year, dayOfYear)

	// 计算连续签到天数
	continuousDays, err = CalculateContinuousDays(userID, year, dayOfYear)
	if err != nil {
		return isChecked, isBroken, 0, fmt.Errorf("计算连续签到天数失败: %v", err)
	}

	return isChecked, isBroken, continuousDays, nil
}
